Skip to content

Verify ACP relay events before prompt routing - #7010

Open
jmecom wants to merge 2 commits into
mainfrom
jm/verify-acp-relay-events
Open

Verify ACP relay events before prompt routing#7010
jmecom wants to merge 2 commits into
mainfrom
jm/verify-acp-relay-events

Conversation

@jmecom

@jmecom jmecom commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What changed

Verify every Nostr EVENT received by buzz-acp immediately after deserialization. Events with an invalid NIP-01 ID or Schnorr signature are dropped before subscription routing, deduplication, replay-watermark updates, membership handling, or either harness queue.

Safety

Signature verification runs on the blocking pool so cryptographic work does not block the relay task. A verification failure drops only that event and keeps the connection available for subsequent valid traffic. The existing observer-control verification remains as defense in depth.

Regression tests cover valid delivery; changed content, ID, signature, pubkey, tags, and timestamp; a forged owner shutdown command with a recomputed ID; forged membership notifications; and forged observer-control events.

Testing

cargo test -p buzz-acp

cargo clippy -p buzz-acp --all-targets -- -D warnings

just ci

Signed-off-by: Jordan Mecom <jm@squareup.com>
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is bc006f67087b049e2f9c4d8a2f26faceff628225...0fec90ae41d21bc92c322e14d386f79585bd837c.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 0fec90ae41d21bc92c322e14d386f79585bd837c to authorize a new review.
Any previous review applies only to its recorded range.

@jmecom
jmecom marked this pull request as ready for review August 28, 2026 23:11
@jmecom
jmecom requested a review from a team as a code owner August 28, 2026 23:11

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed at head 0a05ea3 with two independent source reviews plus live adversarial E2E. The fix itself is solid — one IMPORTANT item is holding approval.

IMPORTANT — the new security regression tests run in no CI lane. The four tests live in crates/buzz-acp/src/relay.rs, but just test-unit's nextest list never runs buzz-acp, and the no-nextest fallback in scripts/run-tests.sh has the same omission. Server Cross-Compile is check/build only, and the Backend Integration nextest archive covers only buzz-db/buzz-relay/buzz-test-client. The green Unit Tests job therefore never executed these tests, which leaves this boundary fix unprotected against later regression. Mutation testing confirmed the tests do observe the production wiring (deleting the verification block fails all three adversarial tests), so wiring them into the gate is all that's needed: add cargo nextest run -p buzz-acp to the test-unit nextest branch and a matching cargo test -p buzz-acp -- --nocapture step to the fallback list. Heads-up: some pre-existing buzz-acp timing tests flaked when the package suite ran locally, so they may need attention when this lands in CI.

Everything else came back clean:

  • Verification sits at the single choke point ahead of observer-control, membership, dedup/watermark, and harness-queue effects; the handshake-buffer replay path re-enters the same handler, so buffered pre-auth events are covered, and there is no other producer into either queue.
  • Live E2E against a malicious relay: a forged owner !shutdown with recomputed ID was dropped with the expected log line, never reached ACP prompt routing, and a correctly signed owner message on the same connection one second later dispatched exactly one prompt.
  • The privilege-sensitive REST fetch paths (huddle instructions, canvas, engram core) already self-verify. One pre-existing, out-of-scope gap: fetch_project_home_for_channel consumes unverified events for prompt metadata — worth a small follow-up, not a blocker here.

Signed-off-by: Jordan Mecom <jm@squareup.com>
@jmecom
jmecom force-pushed the jm/verify-acp-relay-events branch from f6e5d60 to 0fec90a Compare August 31, 2026 18:12
@jmecom

jmecom commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

@buzz-security-review 0fec90a

@Giszmo

Giszmo commented Sep 3, 2026

Copy link
Copy Markdown

Concept ACK 0fec90a, tested locally.

Context: I run several buzz-acp instances for Leo (Giszmo) against relays that the agent host does not operate. For that deployment shape an owner-only respond_to gate that trusts the pubkey field as delivered is not a gate: the relay operator, any relay bug, or a compromised relay host can start turns with owner authority (!shutdown, !rotate, forged membership notifications, arbitrary prompts). This PR closes exactly that hole, and it is the difference between "the relay can drop or delay my events" and "the relay can act as my owner".

What I checked at head 0fec90a (merges cleanly onto today's main c6ca9d9):

  • Placement. Verification is the first statement in the RelayMessage::Event arm of handle_ws_message, ahead of the observer-control, membership and channel branches, ahead of every seen_ids / last_seen / membership_last_seen update, and ahead of both mpsc queues. process_handshake_buffer re-encodes buffered frames through the same handler, so pre-AUTH buffered events are covered. I found no other producer into event_tx besides the test-only test_pair.
  • Check strength. buzz_core::verify_event recomputes the NIP-01 id and checks the Schnorr signature. It runs on spawn_blocking and is awaited sequentially, so a hostile relay can only slow its own connection; a JoinError also drops the event.
  • Relay-originated events keep working. Membership notifications are signed with state.relay_keypair on the relay side (buzz-relay/src/handlers/side_effects.rs:950-953), so they still verify.
  • Tests. cargo test -p buzz-acp at head: 832 passed, 2 failed, both unrelated and passing in isolation. config::tests::allowed_respond_to_full_path_unset_allows_all fails only when BUZZ_ACP_ALLOWED_RESPOND_TO is set in the host environment (it was on my box), and acp::tests::claude_named_adapter_wire_lifecycle_records_prompt_and_cost hit ETXTBSY (Text file busy) once under parallel execution and passed 3/3 reruns, a pre-existing flake. Mutation: deleting the verification block makes the three adversarial tests fail while the valid-delivery test still passes, so the tests do observe the production wiring.
  • CI. The Unit Tests job on 0fec90a is green and now runs cargo nextest run -p buzz-acp, which resolves the blocking item from the earlier review.

Residual gaps, all out of scope for this PR but worth stating so nobody reads it as "the relay is untrusted now":

  1. Verification proves authorship, not authorization or placement. A relay can still deliver an authentic event under a different subscription id (channel relabeling), and can still emit membership notifications for arbitrary channels because nothing checks the notification signer against a relay trust anchor. Verify ACP query events and bind channel routing #7012 covers the relabeling part.
  2. The HTTP /query history path (conversation, thread and DM context, fetch_project_home_for_channel) stays unverified until Verify ACP query events and bind channel routing #7012.
  3. Replay of old authentic events by the relay is bounded only by the since watermarks and the two-generation dedup.
  4. The CLI's buzz messages get preserves sig since fix(cli): preserve signatures in event reads #6884 but does not verify, so an agent that reads history through the CLI mid-turn still gets relay-trusted content. That deserves its own fix.

Why concept ACK rather than approval: I reviewed the diff, ran the tests and the mutation locally, but I have not exercised it against a live malicious relay and I am not a maintainer of this crate.

Bob, Leo Wandersleb's Claude Code agent; posted from Leo's account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants